package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"go-har/utils"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"syscall"

	"github.com/mrichman/hargo"
)

type Args struct {
	Mode  string // 模式
	Path  string // 加载har文件所在的目录
	Dns   string // 本地DNS替换
	Rep   string // 替换响应中的内容
	Port  int    // 监听端口，http模式80，https默认443
	Https bool   // 使用https而不是http监听
	Help  bool   // 显示本帮助信息然后退出
}

func initArgs() Args {
	args := Args{}
	flag.StringVar(&args.Mode, "mode", "dump", "run mode, val: dump, fetch, run, serve")
	flag.StringVar(&args.Path, "path", "", "path for har files")
	flag.StringVar(&args.Dns, "dns", "", "temp bind dns in /etc/hosts in local sys")
	flag.StringVar(&args.Rep, "rep", "", "replace str in response, eg:old1$new1$old2$new2...")
	flag.IntVar(&args.Port, "port", 0, "port for serve")
	flag.BoolVar(&args.Https, "https", false, "serve as https")
	flag.BoolVar(&args.Help, "help", false, "show this help info")
	flag.Parse()
	// flag.PrintDefaults()
	if args.Help || args.Mode == "" {
		flag.Usage()
		os.Exit(0)
	}
	if args.Path == "" {
		args.Path, _ = os.Getwd()
	}
	if args.Port == 0 {
		args.Port = 80
		if args.Https {
			args.Port = 443
		}
	}
	return args
}

func dumpPath(path string) {
	fileInfos, err := utils.ListFiles(path, ".*\\.har$", false)
	if err != nil {
		log.Println("报错：" + err.Error())
		os.Exit(-1)
	}
	if len(fileInfos) > 0 {
		log.Printf("发现%v个har文件，开始加载", len(fileInfos))
	} else {
		log.Println("没有发现har文件，程序退出。如需帮助请加--help参数执行")
		os.Exit(0)
	}
	for _, oneInfo := range fileInfos {
		file, err := os.Open(path + "/" + oneInfo.Name())
		if err != nil {
			log.Println("报错：" + err.Error())
			continue
		}
		log.Println("加载har文件：" + oneInfo.Name())
		reader := bufio.NewReader(file)
		har, err := hargo.Decode(reader)
		if err != nil {
			log.Println("报错：" + err.Error())
			file.Close()
			continue
		}
		log.Printf("共加载到%v个条目", len(har.Log.Entries))
		dupMap := make(map[string]int)
		for _, oneEntry := range har.Log.Entries {
			u, err := url.Parse(oneEntry.Request.URL)
			if err == nil {
				if _, ok := dupMap[u.Path]; ok {
					continue
				}
				dupMap[u.Path] = 1
				log.Println("one req: " + u.Host + u.Path)
			}
		}
		file.Close()
	}
}

func fetchPath(path string) {
	fileInfos, err := utils.ListFiles(path, ".*\\.har$", false)
	if err != nil {
		log.Println("报错：" + err.Error())
		os.Exit(-1)
	}
	if len(fileInfos) > 0 {
		log.Printf("发现%v个har文件，开始加载", len(fileInfos))
	} else {
		log.Println("没有发现har文件，程序退出。如需帮助请加--help参数执行")
		os.Exit(0)
	}
	for _, oneInfo := range fileInfos {
		file, err := os.Open(path + "/" + oneInfo.Name())
		if err != nil {
			log.Println("报错：" + err.Error())
			continue
		}
		reader := bufio.NewReader(file)
		err = hargo.Fetch(reader)
		if err != nil {
			log.Println("报错：" + err.Error())
		}
		file.Close()
	}
}

func runPath(path string) {
	fileInfos, err := utils.ListFiles(path, ".*\\.har$", false)
	if err != nil {
		log.Println("报错：" + err.Error())
		os.Exit(-1)
	}
	if len(fileInfos) > 0 {
		log.Printf("发现%v个har文件，开始加载", len(fileInfos))
	} else {
		log.Println("没有发现har文件，程序退出。如需帮助请加--help参数执行")
		os.Exit(0)
	}
	for _, oneInfo := range fileInfos {
		file, err := os.Open(path + "/" + oneInfo.Name())
		if err != nil {
			log.Println("报错：" + err.Error())
			continue
		}
		reader := bufio.NewReader(file)
		err = hargo.Run(reader, false, true)
		if err != nil {
			log.Println("报错：" + err.Error())
		}
		file.Close()
	}
}

func servePath(path string, port int, dns, rep string, https bool) {
	fileInfos, err := utils.ListFiles(path, ".*\\.har$", false)
	if err != nil {
		log.Println("报错：" + err.Error())
		os.Exit(-1)
	}
	if len(fileInfos) > 0 {
		log.Printf("发现%v个har文件，开始加载", len(fileInfos))
	} else {
		log.Println("没有发现har文件，程序退出。如需帮助请加--help参数执行")
		os.Exit(0)
	}
	urlCacheMap := make(map[string][]string)
	count := 0
	for _, oneInfo := range fileInfos {
		log.Println("加载har文件：" + oneInfo.Name())
		cacheMeta, err := cacheHar(path, filepath.Join(path, oneInfo.Name()))
		if err != nil {
			log.Println("缓存har时报错：" + err.Error())
			continue
		}
		for keyStr, subFiles := range cacheMeta.Files {
			urlCacheMap[keyStr] = append(urlCacheMap[keyStr], subFiles...)
			count += len(subFiles)
		}
	}
	log.Printf("共加载%v个地址，共计%v个条目", len(urlCacheMap), count)

	repMap := make(map[string]string)
	if rep != "" {
		blocks := strings.Split(rep, "$")
		if len(blocks)%2 != 0 {
			log.Println("给定的rep不是双数个，请检查，中间用$隔开，本次已跳过rep替换")
		} else {
			for idx := 0; idx < len(blocks); idx += 2 {
				repMap[blocks[idx]] = blocks[idx+1]
			}
			log.Printf("共加载%v个rep替换项，将在返回响应页面时进行替换", len(repMap))
		}
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// log.Printf("HandleFunc: " + r.URL.Path)
		var sb strings.Builder
		if r.URL.Path == "/_harList" {
			// TODO 改为缓存之后，这里的key变成了md5值，显示出来没什么意义了，需要考虑如何调整
			sb.WriteString("<table style='border: 1px solid black'><thead><td>URL</td></thead><tbody>")
			for keyStr := range urlCacheMap {
				sb.WriteString("<tr>")
				sb.WriteString("<td>")
				sb.WriteString(keyStr)
				sb.WriteString("</td>")
				sb.WriteString("</tr>")
			}
			sb.WriteString("</tbody></table>")
			w.Write([]byte(sb.String()))
			return
		} else {
			postData, _ := io.ReadAll(r.Body)
			keyStr := getRequestKey(r.URL, string(postData))
			if subFiles, ok := urlCacheMap[keyStr]; ok && len(subFiles) > 0 {
				// log.Printf("找到<%s>，中标<%v>个子页面，第一个子页面缓存文件为：%s",
				// 	r.URL.Path, len(subFiles), subFiles[0])
				// TODO 设置响应头
				// 输出响应正文
				cacheFile := filepath.Join(path, ".cache", subFiles[0])
				cachedData, err := os.ReadFile(cacheFile)
				if err == nil {
					// w.Write([]byte(strings.ReplaceAll(string(cachedData), "//cis-static-prod.oss-cn-shanghai.aliyuncs.com", "")))
					if len(repMap) > 0 {
						cachedStr := string(cachedData)
						findCount := 0
						for k, v := range repMap {
							if strings.Contains(cachedStr, k) {
								findCount++
								cachedStr = strings.ReplaceAll(cachedStr, k, v)
							}
						}
						if findCount > 0 {
							log.Printf("请求%s时替换%v个不同的串", r.URL.String(), findCount)
							cachedData = []byte(cachedStr)
						}
					}
					w.Write(cachedData)
					return
				} else {
					log.Println("Read cache file " + cacheFile + " error: " + err.Error())
				}
			}
			//
			log.Printf("Not Found <%s>, url params: %s", r.URL.Path, r.URL.RawQuery)
			w.WriteHeader(http.StatusNotFound)
		}
	})
	bindAddr := fmt.Sprintf(":%v", port)
	log.Println("监听地址<" + bindAddr + ">")
	// temp bind dns in /etc/hosts
	if dns != "" {
		clearDns()
		addDns(dns)
	}
	// if https, create creit first, check file exists
	// openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
	if https {
		if !utils.PathExists("cert/cert.pem") || !utils.PathExists("cert/key.pem") {
			log.Println("https证书未找到，无法以https方式启动web服务")
			os.Exit(-1)
		}
		/*
		 https://stackoverflow.com/questions/7580508/getting-chrome-to-accept-self-signed-localhost-certificate

		 For localhost only (Chrome 119 and above)
		 chrome://flags/#temporary-unexpire-flags-m118
		 Enable and relaunch Chrome

		 For localhost only (Chrome 118 and below)
		 chrome://flags/#allow-insecure-localhost
		 Enable and relaunch Chrome
		*/
		err = http.ListenAndServeTLS(bindAddr, "cert/cert.pem", "cert/key.pem", nil)
	} else {
		err = http.ListenAndServe(bindAddr, nil)
	}
	if err != nil {
		log.Println("报错：" + err.Error())
	}
}

type CacheMeta struct {
	Md5   string              `json:"md5"`
	Files map[string][]string `json:"files"` // key是url的路径、url参数、post正文拼起来的哈希值，value是缓存文件的文件名
}

func NewCacheMeta() *CacheMeta {
	return &CacheMeta{Files: make(map[string][]string)}
}

func (_this *CacheMeta) AddEntry(cachePath, harName string, oneEntry *hargo.Entry) error {
	u, err := url.Parse(oneEntry.Request.URL)
	if err != nil {
		return err
	}

	content := []byte(oneEntry.Response.Content.Text)
	subMd5Str, err := utils.BytesToMd5(content)
	if err != nil {
		return err
	}
	subFileName := filepath.Join(harName, subMd5Str+".txt")
	subFilePath := filepath.Join(cachePath, subFileName)
	err = os.WriteFile(subFilePath, content, 0644)
	if err != nil {
		return err
	}

	// log.Println("one req: " + u.Path)
	keyStr := getRequestKey(u, oneEntry.Request.PostData.Text)
	_this.Files[keyStr] = append(_this.Files[keyStr], subFileName)
	return nil
}

func cacheHar(basePath, filePath string) (*CacheMeta, error) {
	// 计算文件的md5
	md5Str, err := utils.GetFileMd5Str(filePath)
	if err != nil {
		return nil, err
	}

	// 检查缓存是否存在，是否为最新内容
	cacheIsOK := false
	cacheMeta := NewCacheMeta()
	harName := filepath.Base(filePath)
	cachePath := filepath.Join(basePath, ".cache")
	cacheHarPath := filepath.Join(cachePath, harName)
	cacheMetaFile := filepath.Join(cacheHarPath, "meta.json")
	if utils.PathExists(cacheMetaFile) {
		metaStr, err := os.ReadFile(cacheMetaFile)
		if err == nil {
			err = json.Unmarshal([]byte(metaStr), cacheMeta)
			if err == nil {
				if md5Str == cacheMeta.Md5 {
					cacheIsOK = true
				}
			}
		}
		if !cacheIsOK {
			os.RemoveAll(cacheMetaFile)
		}
	}

	// 如果缓存ok，则返回缓存信息即可
	if cacheIsOK {
		return cacheMeta, nil
	}

	// 否则，需要生成缓存
	cacheMeta.Md5 = md5Str

	err = os.MkdirAll(cacheHarPath, 0755)
	if err != nil {
		return nil, err
	}

	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}

	reader := bufio.NewReader(file)
	har, err := hargo.Decode(reader)
	if err != nil {
		file.Close()
		return nil, err
	}
	for _, oneEntry := range har.Log.Entries {
		// log.Println("one req: " + oneEntry.Request.URL)
		cacheMeta.AddEntry(cachePath, harName, &oneEntry)
	}
	file.Close()

	// 保存缓存文件
	content, _ := json.Marshal(&cacheMeta)
	err = os.WriteFile(cacheMetaFile, content, 0644)
	if err != nil {
		return nil, err
	}

	return cacheMeta, nil
}

func getRequestKey(u *url.URL, postData string) string {
	urlParamsStr := ""
	// if strings.Contains(outerKey, "/chain") {
	// 	log.Println("Debug")
	// }

	urlParams := u.Query()
	var sb strings.Builder
	for key, values := range urlParams {
		if len(values) == 0 {
			continue
		}
		if values[0] == "" && utils.IsTimestampMS(key) {
			// log.Println("跳过时间戳参数：" + key)
			continue
		} else if (key == "t" || key == "tm") && utils.IsTimestampMS(values[0]) {
			// log.Println("跳过时间戳参数：" + key + "=" + values[0])
			continue
		}

		sb.WriteString(key + "=" + values[0])
	}
	if sb.Len() > 0 {
		urlParamsStr = sb.String()
	}
	key := fmt.Sprintf("%s@@@@%s@@@@%s", u.Path, urlParamsStr, postData)
	md5Str, _ := utils.StrToMd5(key)
	return md5Str
}

const (
	DnsHeadLine    = "\n########@go-har:dns:begin@########\n"
	DnsTailLine    = "\n########@go-har:dns:end@########\n"
	HostsFileWin   = "C://windows/system32/drivers/etc/hosts"
	HostsFileLinux = "/etc/hosts"
)

func getHostsFile() string {
	if runtime.GOOS == "windows" {
		return HostsFileWin
	}
	return HostsFileLinux
}

func addDns(dns string) {
	lines := strings.Split(dns, ",")

	oldBytes, err := os.ReadFile(getHostsFile())
	if err != nil {
		log.Println("报错：" + err.Error())
		return
	}

	var sb strings.Builder
	sb.WriteString(string(oldBytes))
	sb.WriteString(DnsHeadLine)
	for _, line := range lines {
		sb.WriteString(fmt.Sprintf("127.0.0.1 %s\n", line))
	}
	sb.WriteString(DnsTailLine)
	err = os.WriteFile(getHostsFile(), []byte(sb.String()), 0644)
	if err != nil {
		log.Println("报错：" + err.Error())
	}
}

func clearDns() {
	oldBytes, err := os.ReadFile(getHostsFile())
	if err != nil {
		log.Println("报错：" + err.Error())
		return
	}
	r, err := regexp.Compile(DnsHeadLine + "(?s).*" + DnsTailLine)
	if err != nil {
		log.Println("报错：" + err.Error())
		return
	}
	oldStr := string(oldBytes)
	str := r.ReplaceAllString(oldStr, "")
	if oldStr != str {
		err = os.WriteFile(getHostsFile(), []byte(str), 0644)
		if err != nil {
			log.Println("报错：" + err.Error())
		}
	}
}

func selectMode(args Args) {
	if args.Mode == "dump" {
		dumpPath(args.Path)
	} else if args.Mode == "fetch" {
		fetchPath(args.Path)
	} else if args.Mode == "run" {
		runPath(args.Path)
	}
}

func main() {
	args := initArgs()

	if args.Mode == "serve" {
		go servePath(args.Path, args.Port, args.Dns, args.Rep, args.Https)

		ch := make(chan os.Signal, 1)
		signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
		log.Println(<-ch)

		clearDns()
	} else {
		selectMode(args)
	}
}
